package apig.algorithm.fingerprints;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;

import apig.algorithm.Algorithm;
import apig.algorithm.ProgressMonitor;
import apig.algorithm.tokenizer.JavaStringTokenizer;
import apig.algorithm.tokenizer.JavaTokenizer;
import apig.algorithm.tokenizer.MalformedCodeException;
import apig.algorithm.tokenizer.Tokenizer.Token;
import apig.core.ServerThings;

/**
 * <b>Note:</b> thread SAFE.
 * <p>
 * <b> Note:</b> Doesn't create threads.
 * 
 * @author Alex
 */
public class FingerprintsAlgorithm implements Algorithm {

	private static final int K = 31;
	private static final int T = 60;
	private static final int W = T - K + 1;
	private static final long HASH_POW = 89528293;
	private static final long HASH_MAX;
	static {
		long p = 1;
		for (int i = 0; i < K - 1; ++i)
			p *= HASH_POW;
		HASH_MAX = p;
	}

	static final Object PRESENTATION_LOCK = new Object();

	@Override
	public void inspect(Connection c, int codeId, Iterable<Integer> codeIds,
			ProgressMonitor pm) throws SQLException {
		preparePresentations(codeId);
		if (pm != null)
			pm.setProgress(0.5);
		try (Statement st = c.createStatement();
				Statement st1 = c.createStatement();) {
			ResultSet rs;
			rs = st.executeQuery("select f2.code_id, f1.start, f1.end, f2.start, f2.end, length(c1.code), length(c2.code) from fingerprints f1 join fingerprints f2 on f1.hash = f2.hash join codes c2 on f2.code_id = c2.code_id, codes c1 where f1.code_id = "
					+ codeId
					+ " and f1.code_id > f2.code_id and c1.user_id != c2.user_id and c1.code_id = "
					+ codeId + " order by 1");
			if (pm != null)
				pm.setProgress(0.75);

			int codeId1 = -1;
			int length1 = -1;
			int length2 = -1;
			List<Hypo> list = new ArrayList<Hypo>();
			for (;;) {
				boolean haveNext = rs.next();
				if (codeId1 != -1 && (!haveNext || rs.getInt(1) != codeId1)) {
					compress(list);
					double veracity = getVeracity(list, length1, length2);
					for (Hypo hypo : list)
						st1.execute("insert into hypos values(next value for hypos,"
								+ codeId
								+ ","
								+ codeId1
								+ ","
								+ veracity
								+ ","
								+ asCsv(hypo) + ")");
					list.clear();
				}
				if (!haveNext)
					break;
				codeId1 = rs.getInt(1);
				length1 = rs.getInt(6);
				length2 = rs.getInt(7);
				list.add(new Hypo(rs.getInt(2), rs.getInt(3), rs.getInt(4), rs
						.getInt(5)));
			}
		}
	}

	void preparePresentations(int codeId) throws SQLException {
		synchronized (PRESENTATION_LOCK) {
			try (Connection c = ServerThings.getThings().getConnection();
					Statement st = c.createStatement();
					Statement st1 = c.createStatement();) {
				ResultSet rs;
				st.execute("create table if not exists fingerprints (hash bigint not null, code_id int not null, start int not null, end int not null)");
				st.execute("create index if not exists hash on fingerprints(hash)");
				st.execute("create index if not exists code_id on fingerprints(code_id)");
				rs = st.executeQuery("select code_id, code from codes where code_id between (select ifnull(max(code_id), -1) from fingerprints) + 1 and "
						+ codeId);
				JavaTokenizer tok = new JavaTokenizer();
				while (rs.next()) {
					MinQueue<Long> q = new MinQueue<Long>(W);
					int id = rs.getInt(1);
					Token[] tokens;
					try {
						tokens = tok.parse(rs.getString(2));
					} catch (MalformedCodeException e) {
						e.printStackTrace();
						continue;
					}
					long hash = 0;
					int prevPos = -1;
					for (int i = 0; i < tokens.length; ++i) {
						hash = hash * HASH_POW + tokens[i].getID();
						if (K - 1 <= i) {
							q.push(hash);
							if (T - 1 <= i) {
								int pos = q.getMinPos() + i + 1 - T;
								if (pos != prevPos) {
									prevPos = pos;
									st1.execute("insert into fingerprints values("
											+ q.getMin()
											+ ","
											+ id
											+ ","
											+ tokens[pos].getStart()
											+ ","
											+ tokens[pos + K - 1].getEnd()
											+ ")");
								}
								q.pop();
							}
							hash -= HASH_MAX * tokens[i + 1 - K].getID();
						}
					}
				}
				rs.close();
				c.commit();
			}
		}
	}

	private static double getVeracity(List<? extends Hypo> list, int length1,
			int length2) {
		int sum1 = 0;
		int sum2 = 0;
		for (Hypo h : list) {
			sum1 += h.end1 - h.start1;
			sum2 += h.end2 - h.start2;
		}
		double x = Math.max(sum1 * 1. / length1, sum2 * 1. / length2);
		x = (x - 0.1) / (0.6 - 0.1);
		return Math.min(Math.max(x, 0), 1);
	}

	public static void compress(final List<Hypo> list) {
		List<HypoInfo> hypos = new ArrayList<HypoInfo>(list.size());
		for (int i = 0; i < list.size(); ++i)
			hypos.add(new HypoInfo(list.get(i)));
		list.clear();
		for (int num = 0; num < 2; ++num) {
			final int num1 = num;
			Collections.sort(hypos, new Comparator<HypoInfo>() {

				@Override
				public int compare(HypoInfo h1, HypoInfo h2) {
					return Integer.compare(h1.getStart(num1), h2.getStart(num1));
				}
			});
			int ind = 0;
			for (int i = 0; i < hypos.size(); ++ind) {
				do {
					hypos.get(i).ind[num] = ind;
					++i;
				} while (i < hypos.size()
						&& hypos.get(i - 1).getEnd(num) >= hypos.get(i)
								.getStart(num));
			}
		}
		Collections.sort(hypos, new Comparator<HypoInfo>() {

			@Override
			public int compare(HypoInfo h1, HypoInfo h2) {
				return HypoInfo.indCompare(h1.ind, h2.ind);
			}
		});
		for (int i = 0; i < hypos.size();) {
			Hypo hypo = new Hypo(Integer.MAX_VALUE, Integer.MIN_VALUE,
					Integer.MAX_VALUE, Integer.MIN_VALUE);
			do {
				hypo.start1 = Math.min(hypo.start1, hypos.get(i).h.start1);
				hypo.end1 = Math.max(hypo.end1, hypos.get(i).h.end1);
				hypo.start2 = Math.min(hypo.start2, hypos.get(i).h.start2);
				hypo.end2 = Math.max(hypo.end2, hypos.get(i).h.end2);
				++i;
			} while (i < hypos.size()
					&& HypoInfo.indCompare(hypos.get(i - 1).ind,
							hypos.get(i).ind) == 0);
			list.add(hypo);
		}
	}

	public static String asCsv(Hypo h) {
		return h.start1 + "," + h.end1 + "," + h.start2 + "," + h.end2;
	}

	public static void main(String[] args) throws SQLException {
		try (Connection c = ServerThings.getThings().getConnection();) {
			try (Statement st = c.createStatement();) {
				st.executeUpdate("drop table fingerprints");
				st.executeUpdate("truncate table hypos");
				st.executeUpdate("update codes set checked = false");
			}
			FingerprintsAlgorithm algo = new FingerprintsAlgorithm();
			algo.inspect(c, 7, null, null);
			algo.inspect(c, 8, null, null);
			algo.inspect(c, 6, null, null);
			c.commit();
			visualize(c);
		}
	}

	private static void visualize(Connection c) throws SQLException {
		final List<CodeHypo> hypos = new ArrayList<CodeHypo>();
		int code1Id = 8;
		int code2Id = 6;
		String code1;
		String code2;
		try (Statement st = c.createStatement();) {
			ResultSet rs;
			rs = st.executeQuery("select distinct code1_id, code2_id from hypos order by 1, 2");
			while (rs.next())
				System.out.println(rs.getInt(1) + " " + rs.getInt(2));
			rs = st.executeQuery("select code1_begin, code1_end, code2_begin, code2_end from hypos where code1_id = "
					+ code1Id
					+ " and code2_id = "
					+ code2Id
					+ " order by 1, 3, 2, 4");
			while (rs.next())
				hypos.add(new CodeHypo(rs.getInt(1), rs.getInt(2),
						rs.getInt(3), rs.getInt(4)));
			rs = st.executeQuery("select code from codes where code_id = "
					+ code1Id);
			rs.next();
			code1 = rs.getString(1);
			rs = st.executeQuery("select code from codes where code_id = "
					+ code2Id);
			rs.next();
			code2 = rs.getString(1);
		}
		new JVisualFrame(code1, code2, hypos).setVisible(true);
	}
}

class HypoInfo {
	Hypo h;
	int[] ind = new int[2];

	public HypoInfo(Hypo h) {
		this.h = h;
	}

	public int getStart(int n) {
		return n == 0 ? h.start1 : h.start2;
	}

	public int getEnd(int n) {
		return n == 0 ? h.end1 : h.end2;
	}

	static int indCompare(int[] ind1, int[] ind2) {
		int c = Integer.compare(ind1[0], ind2[0]);
		return c == 0 ? Integer.compare(ind1[1], ind2[1]) : c;
	}
}

class JVisualFrame extends JFrame {
	int hypo = 0;
	private static final DefaultHighlightPainter painter = new DefaultHighlightPainter(
			new Color(0x40FF80));
	private final List<CodeHypo> hypos;

	public JVisualFrame(String code1, String code2, final List<CodeHypo> hypos) {
		this.hypos = hypos;
		JButton next = new JButton("next");
		JButton prev = new JButton("prev");
		final JTextArea text1 = new JTextArea(code1);
		JScrollPane text1Scroll = new JScrollPane(text1);
		text1Scroll.setPreferredSize(new Dimension(400, 400));
		final JTextArea text2 = new JTextArea(code2);
		JScrollPane text2Scroll = new JScrollPane(text2);
		text2Scroll.setPreferredSize(new Dimension(400, 400));
		setLayout(new FlowLayout());
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		add(next);
		add(prev);
		add(text1Scroll);
		add(text2Scroll);
		next.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				if (hypos.size() - 1 == hypo)
					return;
				++hypo;
				drawChange(text1, text2);
			}
		});
		prev.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (hypo == 0)
					return;
				--hypo;
				drawChange(text1, text2);
			}
		});
		pack();
		drawChange(text1, text2);
	}

	private void drawChange(final JTextArea text1, final JTextArea text2) {
		try {
			if (!hypos.isEmpty()) {
				CodeHypo h = hypos.get(hypo);
				text1.getHighlighter().removeAllHighlights();
				text1.getHighlighter().addHighlight(h.b1, h.e1, painter);
				text1.setCaretPosition(h.b1);
				text1.setCaretPosition(h.e1);
				text2.getHighlighter().removeAllHighlights();
				text2.getHighlighter().addHighlight(h.b2, h.e2, painter);
				text2.setCaretPosition(h.b2);
				text2.setCaretPosition(h.e2);
			}
		} catch (BadLocationException e1) {
		}
	}
}

class CodeHypo {
	int b1, e1, b2, e2;

	public CodeHypo(int b1, int e1, int b2, int e2) {
		this.b1 = b1;
		this.e1 = e1;
		this.b2 = b2;
		this.e2 = e2;
	}
}
